/*
 * libid3tag - ID3 tag manipulation library
 * Copyright (C) 2000-2004 Underbit Technologies, Inc.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * $Id: frame.c,v 1.15 2004/01/23 09:41:32 rob Exp $
 */

# ifdef HAVE_CONFIG_H
#  include "config.h"
# endif

# include "global.h"

# include <stdlib.h>
# include <string.h>

# ifdef HAVE_ASSERT_H
#  include <assert.h>
# endif

# include "id3tag.h"
# include "frame.h"
# include "frametype.h"
# include "compat.h"
# include "field.h"
# include "render.h"
# include "parse.h"
# include "util.h"

static
int valid_idchar(char c)
{
    return (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9');
}

/*
 * NAME:    frame->validid()
 * DESCRIPTION: return true if the parameter string is a legal frame ID
 */
int id3_frame_validid(char const *id)
{
    return id &&
           valid_idchar(id[0]) &&
           valid_idchar(id[1]) &&
           valid_idchar(id[2]) &&
           valid_idchar(id[3]);
}

/*
 * NAME:    frame->new()
 * DESCRIPTION: allocate and return a new frame
 */
struct id3_frame *id3_frame_new(char const *id)
{
    struct id3_frametype const *frametype;
    struct id3_frame           *frame;
    unsigned int               i;

    if (!id3_frame_validid(id))
        return 0;

    frametype = id3_frametype_lookup(id, 4);
    if (frametype == 0)
    {
        switch (id[0])
        {
        case 'T':
            frametype = &id3_frametype_text;
            break;

        case 'W':
            frametype = &id3_frametype_url;
            break;

        case 'X':
        case 'Y':
        case 'Z':
            frametype = &id3_frametype_experimental;
            break;

        default:
            frametype = &id3_frametype_unknown;
            if (id3_compat_lookup(id, 4))
                frametype = &id3_frametype_obsolete;
            break;
        }
    }

    frame = malloc(sizeof(*frame) + frametype->nfields * sizeof(*frame->fields));
    if (frame)
    {
        frame->id[0] = id[0];
        frame->id[1] = id[1];
        frame->id[2] = id[2];
        frame->id[3] = id[3];
        frame->id[4] = 0;

        frame->description       = frametype->description;
        frame->refcount          = 0;
        frame->flags             = frametype->defaultflags;
        frame->group_id          = 0;
        frame->encryption_method = 0;
        frame->encoded           = 0;
        frame->encoded_length    = 0;
        frame->decoded_length    = 0;
        frame->nfields           = frametype->nfields;
        frame->fields            = (union id3_field *) &frame[1];

        for (i = 0; i < frame->nfields; ++i)
            id3_field_init(&frame->fields[i], frametype->fields[i]);
    }

    return frame;
}

void id3_frame_delete(struct id3_frame *frame)
{
    assert(frame);

    if (frame->refcount == 0)
    {
        unsigned int i;

        for (i = 0; i < frame->nfields; ++i)
            id3_field_finish(&frame->fields[i]);

        if (frame->encoded)
            free(frame->encoded);

        free(frame);
    }
}

/*
 * NAME:    frame->addref()
 * DESCRIPTION: add an external reference to a frame
 */
void id3_frame_addref(struct id3_frame *frame)
{
    assert(frame);

    ++frame->refcount;
}

/*
 * NAME:    frame->delref()
 * DESCRIPTION: remove an external reference to a frame
 */
void id3_frame_delref(struct id3_frame *frame)
{
    assert(frame && frame->refcount > 0);

    --frame->refcount;
}

/*
 * NAME:    frame->field()
 * DESCRIPTION: return a pointer to a field in a frame
 */
union id3_field *id3_frame_field(struct id3_frame const *frame,
                                 unsigned int index)
{
    assert(frame);

    return (index < frame->nfields) ? &frame->fields[index] : 0;
}

static
struct id3_frame *obsolete(char const *id, id3_byte_t const *data,
                           id3_length_t length)
{
    struct id3_frame *frame;

    frame = id3_frame_new(ID3_FRAME_OBSOLETE);
    if (frame)
    {
        if (id3_field_setframeid(&frame->fields[0], id) == -1 ||
            id3_field_setbinarydata(&frame->fields[1], data, length) == -1)
            goto fail;
    }

    if (0)
    {
 fail:
        if (frame)
        {
            id3_frame_delete(frame);
            frame = 0;
        }
    }

    return frame;
}

static
struct id3_frame *unparseable(char const *id, id3_byte_t const **ptr,
                              id3_length_t length, int flags,
                              int group_id, int encryption_method,
                              id3_length_t decoded_length)
{
    struct id3_frame *frame = 0;
    id3_byte_t       *mem;

    mem = malloc(length ? length : 1);
    if (mem == 0)
        goto fail;

    frame = id3_frame_new(id);
    if (frame == 0)
        free(mem);
    else
    {
        memcpy(mem, *ptr, length);

        frame->flags             = flags;
        frame->group_id          = group_id;
        frame->encryption_method = encryption_method;
        frame->encoded           = mem;
        frame->encoded_length    = length;
        frame->decoded_length    = decoded_length;
    }

    if (0)
    {
 fail:
        ;
    }

    *ptr += length;

    return frame;
}

static
int parse_data(struct id3_frame *frame,
               id3_byte_t const *data, id3_length_t length)
{
    enum id3_field_textencoding encoding;
    id3_byte_t const            *end;
    unsigned int                i;

    encoding = ID3_FIELD_TEXTENCODING_ISO_8859_1;

    end = data + length;

    for (i = 0; i < frame->nfields; ++i)
    {
        if (id3_field_parse(&frame->fields[i], &data, end - data, &encoding) == -1)
            return -1;
    }

    return 0;
}

/*
 * NAME:    frame->parse()
 * DESCRIPTION: parse raw frame data according to the specified ID3 tag version
 */
struct id3_frame *id3_frame_parse(id3_byte_t const **ptr, id3_length_t length,
                                  unsigned int version)
{
    struct id3_frame        *frame = 0;
    id3_byte_t const        *id, *end, *data;
    id3_length_t            size, decoded_length = 0;
    int                     flags   = 0, group_id = 0, encryption_method = 0;
    struct id3_compat const *compat = 0;
    id3_byte_t              *mem    = 0;
    char                    xid[4];

    id  = *ptr;
    end = *ptr + length;

    if (ID3_TAG_VERSION_MAJOR(version) < 4)
    {
        switch (ID3_TAG_VERSION_MAJOR(version))
        {
        case 2:
            if (length < 6)
                goto fail;

            compat = id3_compat_lookup(id, 3);

            *ptr += 3;
            size  = id3_parse_uint(ptr, 3);

            if (size > end - *ptr)
                goto fail;

            end = *ptr + size;

            break;

        case 3:
            if (length < 10)
                goto fail;

            compat = id3_compat_lookup(id, 4);

            *ptr += 4;
            size  = id3_parse_uint(ptr, 4);
            flags = id3_parse_uint(ptr, 2);

            if (size > end - *ptr)
                goto fail;

            end = *ptr + size;

            if (flags & (ID3_FRAME_FLAG_FORMATFLAGS & ~0x00e0))
            {
                frame = unparseable(id, ptr, end - *ptr, 0, 0, 0, 0);
                goto done;
            }

            flags =
                ((flags >> 1) & ID3_FRAME_FLAG_STATUSFLAGS) |
                ((flags >> 4) & (ID3_FRAME_FLAG_COMPRESSION |
                                 ID3_FRAME_FLAG_ENCRYPTION)) |
                ((flags << 1) & ID3_FRAME_FLAG_GROUPINGIDENTITY);

            if (flags & ID3_FRAME_FLAG_COMPRESSION)
            {
                if (end - *ptr < 4)
                    goto fail;

                decoded_length = id3_parse_uint(ptr, 4);
            }

            if (flags & ID3_FRAME_FLAG_ENCRYPTION)
            {
                if (end - *ptr < 1)
                    goto fail;

                encryption_method = id3_parse_uint(ptr, 1);
            }

            if (flags & ID3_FRAME_FLAG_GROUPINGIDENTITY)
            {
                if (end - *ptr < 1)
                    goto fail;

                group_id = id3_parse_uint(ptr, 1);
            }

            break;

        default:
            goto fail;
        }

        /* canonicalize frame ID for ID3v2.4 */

        if (compat && compat->equiv)
            id = compat->equiv;
        else if (ID3_TAG_VERSION_MAJOR(version) == 2)
        {
            xid[0] = 'Y';
            xid[1] = id[0];
            xid[2] = id[1];
            xid[3] = id[2];

            id = xid;

            flags |=
                ID3_FRAME_FLAG_TAGALTERPRESERVATION |
                ID3_FRAME_FLAG_FILEALTERPRESERVATION;
        }
    }
    else  /* ID3v2.4 */
    {
        if (length < 10)
            goto fail;

        *ptr += 4;
        size  = id3_parse_syncsafe(ptr, 4);
        flags = id3_parse_uint(ptr, 2);

        if (size > end - *ptr)
            goto fail;

        end = *ptr + size;

        if (flags & (ID3_FRAME_FLAG_FORMATFLAGS & ~ID3_FRAME_FLAG_KNOWNFLAGS))
        {
            frame = unparseable(id, ptr, end - *ptr, flags, 0, 0, 0);
            goto done;
        }

        if (flags & ID3_FRAME_FLAG_GROUPINGIDENTITY)
        {
            if (end - *ptr < 1)
                goto fail;

            group_id = id3_parse_uint(ptr, 1);
        }

        if ((flags & ID3_FRAME_FLAG_COMPRESSION) &&
            !(flags & ID3_FRAME_FLAG_DATALENGTHINDICATOR))
            goto fail;

        if (flags & ID3_FRAME_FLAG_ENCRYPTION)
        {
            if (end - *ptr < 1)
                goto fail;

            encryption_method = id3_parse_uint(ptr, 1);
        }

        if (flags & ID3_FRAME_FLAG_DATALENGTHINDICATOR)
        {
            if (end - *ptr < 4)
                goto fail;

            decoded_length = id3_parse_syncsafe(ptr, 4);
        }
    }

    data = *ptr;
    *ptr = end;

    /* undo frame encodings */

    if ((flags & ID3_FRAME_FLAG_UNSYNCHRONISATION) && end - data > 0)
    {
        mem = malloc(end - data);
        if (mem == 0)
            goto fail;

        memcpy(mem, data, end - data);

        end  = mem + id3_util_deunsynchronise(mem, end - data);
        data = mem;
    }

    if (flags & ID3_FRAME_FLAG_ENCRYPTION)
    {
        frame = unparseable(id, &data, end - data, flags,
                            group_id, encryption_method, decoded_length);
        goto done;
    }

    if (flags & ID3_FRAME_FLAG_COMPRESSION)
    {
        id3_byte_t *decomp;

        decomp = id3_util_decompress(data, end - data, decoded_length);
        if (decomp == 0)
            goto fail;

        if (mem)
            free(mem);

        data = mem = decomp;
        end  = data + decoded_length;
    }

    /* check for obsolescence */

    if (compat && !compat->equiv)
    {
        frame = obsolete(id, data, end - data);
        goto done;
    }

    /* generate the internal frame structure */

    frame = id3_frame_new(id);
    if (frame)
    {
        frame->flags    = flags;
        frame->group_id = group_id;

        if (compat && compat->translate)
        {
            if (compat->translate(frame, compat->id, data, end - data) == -1)
                goto fail;
        }
        else
        {
            if (parse_data(frame, data, end - data) == -1)
                goto fail;
        }
    }

    if (0)
    {
 fail:
        if (frame)
        {
            id3_frame_delete(frame);
            frame = 0;
        }
    }

 done:
    if (mem)
        free(mem);

    return frame;
}

static
id3_length_t render_data(id3_byte_t **ptr,
                         union id3_field *fields, unsigned int length)
{
    id3_length_t                size = 0;
    enum id3_field_textencoding encoding;
    unsigned int                i;

    encoding = ID3_FIELD_TEXTENCODING_ISO_8859_1;

    for (i = 0; i < length; ++i)
        size += id3_field_render(&fields[i], ptr, &encoding, i < length - 1);

    return size;
}

/*
 * NAME:    frame->render()
 * DESCRIPTION: render a single, complete frame
 */
id3_length_t id3_frame_render(struct id3_frame const *frame,
                              id3_byte_t **ptr, int options)
{
    id3_length_t size      = 0, decoded_length, datalen;
    id3_byte_t   *size_ptr = 0, *flags_ptr = 0, *data = 0;
    int          flags;

    assert(frame);

    if ((frame->flags & ID3_FRAME_FLAG_TAGALTERPRESERVATION) ||
        ((options & ID3_TAG_OPTION_FILEALTERED) &&
         (frame->flags & ID3_FRAME_FLAG_FILEALTERPRESERVATION)))
        return 0;

    /* a frame must be at least 1 byte big, excluding the header */

    decoded_length = render_data(0, frame->fields, frame->nfields);
    if (decoded_length == 0 && frame->encoded == 0)
        return 0;

    /* header */

    size += id3_render_immediate(ptr, frame->id, 4);

    if (ptr)
        size_ptr = *ptr;

    size += id3_render_syncsafe(ptr, 0, 4);

    if (ptr)
        flags_ptr = *ptr;

    flags = frame->flags;

    size += id3_render_int(ptr, flags, 2);

    if (flags & (ID3_FRAME_FLAG_FORMATFLAGS & ~ID3_FRAME_FLAG_KNOWNFLAGS))
    {
        size += id3_render_binary(ptr, frame->encoded, frame->encoded_length);
        if (size_ptr)
            id3_render_syncsafe(&size_ptr, size - 10, 4);

        return size;
    }

    flags &= ID3_FRAME_FLAG_KNOWNFLAGS;

    flags &= ~ID3_FRAME_FLAG_UNSYNCHRONISATION;
    if (options & ID3_TAG_OPTION_UNSYNCHRONISATION)
        flags |= ID3_FRAME_FLAG_UNSYNCHRONISATION;

    if (!(flags & ID3_FRAME_FLAG_ENCRYPTION))
    {
        flags &= ~ID3_FRAME_FLAG_COMPRESSION;
        if (options & ID3_TAG_OPTION_COMPRESSION)
            flags |= ID3_FRAME_FLAG_COMPRESSION | ID3_FRAME_FLAG_DATALENGTHINDICATOR;
    }

    if (flags & ID3_FRAME_FLAG_GROUPINGIDENTITY)
        size += id3_render_int(ptr, frame->group_id, 1);
    if (flags & ID3_FRAME_FLAG_ENCRYPTION)
        size += id3_render_int(ptr, frame->encryption_method, 1);
    if (flags & ID3_FRAME_FLAG_DATALENGTHINDICATOR)
    {
        if (flags & ID3_FRAME_FLAG_ENCRYPTION)
            decoded_length = frame->decoded_length;
        size += id3_render_syncsafe(ptr, decoded_length, 4);
    }

    if (ptr)
        data = *ptr;

    if (flags & ID3_FRAME_FLAG_ENCRYPTION)
        datalen = id3_render_binary(ptr, frame->encoded, frame->encoded_length);
    else
    {
        if (ptr == 0)
            datalen = decoded_length;
        else
        {
            datalen = render_data(ptr, frame->fields, frame->nfields);

            if (flags & ID3_FRAME_FLAG_COMPRESSION)
            {
                id3_byte_t   *comp;
                id3_length_t complen;

                comp = id3_util_compress(data, datalen, &complen);
                if (comp == 0)
                    flags &= ~ID3_FRAME_FLAG_COMPRESSION;
                else
                {
                    *ptr    = data;
                    datalen = id3_render_binary(ptr, comp, complen);

                    free(comp);
                }
            }
        }
    }

    /* unsynchronisation */

    if (flags & ID3_FRAME_FLAG_UNSYNCHRONISATION)
    {
        if (data == 0)
            datalen *= 2;
        else
        {
            id3_length_t newlen;

            newlen = id3_util_unsynchronise(data, datalen);
            if (newlen == datalen)
                flags &= ~ID3_FRAME_FLAG_UNSYNCHRONISATION;
            else
            {
                *ptr   += newlen - datalen;
                datalen = newlen;
            }
        }
    }

    size += datalen;

    /* patch size and flags */

    if (size_ptr)
        id3_render_syncsafe(&size_ptr, size - 10, 4);
    if (flags_ptr)
        id3_render_int(&flags_ptr, flags, 2);

    return size;
}
